import datetime
from datetime import timedelta
import stripe.error
from django.db import transaction
from django.db.models import F, Sum
from django.db.models import Exists, OuterRef, Q
from django_tenants.utils import schema_context
from rest_framework.decorators import api_view, permission_classes, authentication_classes
from threading import Thread
import logging

from rest_framework.permissions import IsAdminUser
from rest_framework_simplejwt.authentication import JWTAuthentication

from apps.billing.models import Invoice, ClientInvoice, InvoiceServices, increment_invoice_number
from rest_framework import status
from django.utils import timezone
from apps.booking.models import Booking, Appointment, AppointmentClientServices
from rest_framework.response import Response

from apps.booking.types import BookingStatusChoices, AppointmentStatusChoices
from apps.core.utils import date_time as datetime_helper
from apps.notification.models import ProviderReminderSetting
from apps.notification.types import AppointmentReminderTypes, ClientNotesReminderTypes
from apps.payment.utils import billing_helpers
from apps.worker.permissions import IsGcloudCronJob
from apps.users.models import Provider
from apps.location.models import ProviderBillings
from apps.client.models import Client
from datetime import datetime
from .utils.reminder_emails import send_appointment_reminder_email, send_client_notes_reminder_email, \
    send_client_appointment_email_reminder, send_billing_reminder_email
from ..billing.types import ClientInvoiceStatus
from ..billing.views import create_charge_by_stripe

DEFAULT_PROMO = 50.00


@api_view(('GET',))
@permission_classes([IsGcloudCronJob | IsAdminUser])
def invoice_creation(request):
    logger = logging.getLogger(__name__)
    try:
        us_month_range = datetime_helper.get_time_range('month')
        us_next_month_range = datetime_helper.range_of_us_next_month()

        current_month_end = datetime.fromtimestamp(us_month_range[1].timestamp())
        next_month_end = datetime.fromtimestamp(us_next_month_range[1].timestamp())

        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                current_month_bookings = Booking.objects.filter(start_date__range=us_month_range)
                Invoice.objects.filter(bookings__in=current_month_bookings).delete()
                ProviderBillings.objects.filter(created_at__range=us_month_range, provider=provider).delete()

                next_month_bookings = Booking.objects.filter(start_date__range=us_next_month_range)
                Invoice.objects.filter(bookings__in=next_month_bookings).delete()
                ProviderBillings.objects.filter(created_at__range=us_next_month_range, provider=provider).delete()

                if current_month_bookings.exists():
                    try:
                        booking_ids = list(current_month_bookings.values_list('id', flat=True))
                        total_amount = current_month_bookings.aggregate(total_amount=Sum('room_cost'))['total_amount']

                        new_invoice = Invoice.objects.create(
                            invoice_number=increment_invoice_number(),
                            total_amount=total_amount,
                            due_date=current_month_end
                        )
                        new_invoice.bookings.set(booking_ids)
                        ProviderBillings.objects.create(
                            provider=provider,
                            invoice_number=new_invoice.invoice_number,
                            status=new_invoice.status,
                            due_date=new_invoice.due_date,
                            total_amount=new_invoice.total_amount
                        )
                    except:
                        pass

                if next_month_bookings.exists():
                    try:
                        next_month_booking_ids = list(next_month_bookings.values_list('id', flat=True))
                        next_month_total_amount = next_month_bookings.aggregate(total_amount=Sum('room_cost'))[
                            'total_amount']

                        _new_invoice = Invoice.objects.create(
                            invoice_number=increment_invoice_number(),
                            total_amount=next_month_total_amount,
                            due_date=next_month_end
                        )
                        _new_invoice.bookings.set(next_month_booking_ids)
                        ProviderBillings.objects.create(
                            provider=provider,
                            invoice_number=_new_invoice.invoice_number,
                            status=_new_invoice.status,
                            due_date=_new_invoice.due_date,
                            total_amount=_new_invoice.total_amount
                        )
                    except:
                        pass

        return Response("Invoice Created")

    except Exception as e:
        print(f'Invoice Creation Failed because of {str(e)}')
        logger.error('Failed to create invoice with error {}'.format(str(e)))
        return Response(f'Invoice Creation Failed because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def cancelation_balance_reset(request):
    try:
        Provider.objects.all().update(cancelation_balance=10)
        return Response(status=200)
    except Exception as e:
        return Response(f'Failed in cancelation_balance_reset because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def create_client_invoice(request):
    try:
        providers = Provider.objects.all()
        anchor_time = datetime_helper.now() - timedelta(hours=2)

        for provider in providers:
            with schema_context(provider.schema_name):
                today = anchor_time.date()
                valid_invoices = ClientInvoice.objects \
                    .filter(appointment=OuterRef('pk'))
                completed_appointments = Appointment.objects.filter((Q(
                    booking__start_date__date=today) & Q(booking__isnull=False)) | Q(start_date__date=today)) \
                    .exclude(Q(status=AppointmentStatusChoices.CANCELLED.value) |
                             (Q(booking__status=BookingStatusChoices.CANCELED.value) &
                              Q(booking__isnull=True))) \
                    .annotate(exist_invoice=Exists(valid_invoices)) \
                    .filter(exist_invoice=False)
                dt = datetime_helper.now()
                year = f'{abs(dt.year) % 100}'
                month = f'{dt.month:02d}'
                day = f'{dt.day:02d}'

                today_range = datetime_helper.get_time_range()
                invoices_count = ClientInvoice.objects.filter(
                    created_at__range=today_range).count()

                i = 1
                for appointment in completed_appointments:
                    appointment_services = appointment.appointment_services.all()
                    invoice_index = '{:05}'.format(invoices_count + i)
                    invoice_number = f'{year}{month}{day}{invoice_index}'
                    invoice = {
                        'invoice_number': invoice_number,
                        'appointment': appointment,
                    }
                    clients = appointment.clients.all()
                    for client in clients:
                        fee = AppointmentClientServices.objects.filter(appointment=appointment, client=client). \
                            aggregate(total=Sum('fees'))['total']
                        if fee:
                            invoice['fees'] = fee
                        else:
                            invoice['fees'] = 0
                        invoice['client'] = client

                        created_invoice = ClientInvoice.objects.create(**invoice)
                        created_invoice.issue_date = created_invoice.created_at
                        created_invoice.save()
                        appointment = created_invoice.appointment
                        if appointment_services:
                            for service in appointment_services:
                                InvoiceServices.objects.create(invoice=created_invoice, date=appointment.start_date,
                                                               service=service.service,
                                                               fees=service.fees,
                                                               client=service.client)
                    i += 1

        return Response()
    except Exception as e:
        return Response(f'Failed in create_client_invoice because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def appointment_reminder(request):
    try:
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                reminder_setting = ProviderReminderSetting.objects.all().first()
                appointments = None
                if reminder_setting:
                    appointment_type = reminder_setting.appointment_type
                    if appointment_type == str(AppointmentReminderTypes.TWO_DAYS_BEFORE.value):
                        date_time = [(datetime.now() + timedelta(days=1)).date(),
                                     (datetime.now() + timedelta(days=2)).date()]
                        appointments = Appointment.objects.filter(start_date__date__range=date_time,
                                                                  start_time=datetime.now().time(), is_sent=False)

                    elif appointment_type == str(AppointmentReminderTypes.ONE_DAY_BEFORE.value):
                        date_time = [datetime.now().date(),
                                     (datetime.now() + timedelta(days=1)).date()]
                        # time = (datetime.now() + timedelta(hours=1)).time()
                        appointments = Appointment.objects.filter(start_date__date__range=date_time,
                                                                  start_time=datetime.now().time(), is_sent=False)

                    elif appointment_type == str(AppointmentReminderTypes.ONE_HOUR_BEFORE.value):
                        time = [(datetime.now() + timedelta(minutes=5)).time(),
                                (datetime.now() + timedelta(hours=1)).time()]
                        appointments = Appointment.objects.filter(start_date__date=datetime.now().date(),
                                                                  start_time__range=time, is_sent=False)

                    elif appointment_type == str(AppointmentReminderTypes.FIVE_MINUTES_BEFORE.value):
                        date = datetime.now().date()
                        time_range = [datetime.now().time(), (datetime.now() + timedelta(minutes=5)).time()]
                        appointments = Appointment.objects.filter(start_date__date=date,
                                                                  start_time__range=time_range, is_sent=False)

                    elif appointment_type == str(AppointmentReminderTypes.SESSION_START_TIME.value):
                        appointments = Appointment.objects.filter(start_date__date=datetime.now().date(),
                                                                  start_time=datetime.now().time(), is_sent=False)

                    if appointments:
                        thread = Thread(target=send_appointment_reminder_email, args=[provider, appointments])
                        thread.start()
        return Response(200)
    except Exception as e:
        return Response(f'Failed in appointment_reminder because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def client_reminder_api(request):
    try:
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                reminder_setting = ProviderReminderSetting.objects.all().first()
                appointments = None
                if reminder_setting:
                    client_notes_type = reminder_setting.client_notes_type
                    if client_notes_type == str(ClientNotesReminderTypes.SESSION_END_TIME.value):
                        time_range = [datetime.now().time(),
                                      (datetime.now() + timedelta(minutes=5)).time()]
                        appointments = Appointment.objects.filter(start_date__date=datetime.now().date(),
                                                                  end_time__range=time_range, is_sent=False)

                    elif client_notes_type == str(ClientNotesReminderTypes.FIVE_MINUTES_AFTER.value):
                        time_range = [(datetime.now() + timedelta(minutes=5)).time(),
                                      (datetime.now() + timedelta(hours=1)).time()]
                        appointments = Appointment.objects.filter(start_date__date=datetime.now().date(),
                                                                  end_time__range=time_range, is_sent=False)

                    elif client_notes_type == str(ClientNotesReminderTypes.ONE_HOUR_AFTER.value):
                        date_time = [(datetime.now() + timedelta(hours=1)).date(),
                                     (datetime.now() + timedelta(days=1)).date()]
                        appointments = Appointment.objects.filter(start_date__date__range=date_time,
                                                                  end_time=datetime.now().time(), is_sent=False)

                    elif client_notes_type == str(ClientNotesReminderTypes.ONE_DAY_AFTER.value):
                        date_time = [(datetime.now() - timedelta(days=1)).date(),
                                     datetime.now().date()]
                        appointments = Appointment.objects.filter(start_date__date__range=date_time,
                                                                  end_time=datetime.now().time(), is_sent=False)

                    elif client_notes_type == str(ClientNotesReminderTypes.TWO_DAYS_AFTER.value):
                        date_time = [(datetime.now() - timedelta(days=2)).date(), datetime.now().date()]
                        appointments = Appointment.objects.filter(start_date__date__range=date_time,
                                                                  end_time=datetime.now().time(), is_sent=False)

                    if appointments:
                        thread = Thread(target=send_client_notes_reminder_email, args=[provider, appointments])
                        thread.start()
        return Response(200)
    except Exception as e:
        return Response(f'Failed in client_reminder_api because of {str(e)}', status=500)


def is_not_the_future(self, trigger_date):
    now = timezone.localdate(timezone.now())
    then = datetime.strptime(trigger_date, '%Y-%m-%d').date()
    return then <= now


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def charge_invoice_api(request):
    try:
        check_overdue = 'date' not in request.query_params
        trigger_date = request.query_params.get('date', False)
        if trigger_date and not is_not_the_future(trigger_date):
            return Response(
                {'message': 'Please do not select a future date!'},
                status=status.HTTP_406_NOT_ACCEPTABLE)
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                invoices = billing_helpers.filter_invoices(request)
                for invoice in invoices:
                    billing_helpers.charge_invoice(invoice, check_overdue)

        return Response(None, status=status.HTTP_201_CREATED)
    except Exception as e:
        return Response(f'Failed in charge_invoice_api because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def charge_client_autopay(request):
    try:
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                clients = Client.objects.filter(stripe_account__auto_pay=True)
                date = datetime_helper.now().date()
                for client in clients:
                    invoices = ClientInvoice.objects.filter(client=client, due_date__date=date,
                                                            status=ClientInvoiceStatus.UNPAID.value)
                    data = {
                        "card_id": client.stripe_account.auto_pay_card_id,
                        "description": ""
                    }
                    for invoice in invoices:
                        price = invoice.fees
                        try:
                            if invoice.fees > 0:
                                create_charge_by_stripe(client, price, data, invoice.id)
                            else:
                                invoice.status = ClientInvoiceStatus.PAID.value
                                invoice.save()
                        except stripe.error.StripeError as e:
                            break
        return Response(200)
    except Exception as e:
        return Response(f'Failed in charge_client_autopay because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def appointment_email_reminder(request):
    try:
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                clients = Client.objects.filter(appointment_email_reminder=True)
                now = datetime_helper.now()
                for client in clients:
                    appointments = Appointment.objects.filter(clients=client, start_date__date=now.date(),
                                                              start_time=now.time() + timedelta(minutes=10))
                    if appointments:
                        send_client_appointment_email_reminder(client, appointments)
        return Response(200)
    except Exception as e:
        return Response(f'Failed in appointment_email_reminder because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob])
@transaction.atomic
def client_billing_reminder(request):
    try:
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):
                clients = Client.objects.filter(billing_email_reminder=True)
                now = datetime_helper.now()
                for client in clients:
                    invoices = ClientInvoice.objects.filter(client=client, due_date__date=now.date())
                    for invoice in invoices:
                        send_billing_reminder_email(client, invoice)

        return Response(200)
    except Exception as e:
        return Response(f'Failed in client_billing_reminder because of {str(e)}', status=500)


@api_view(('GET',))
@permission_classes([IsGcloudCronJob | IsAdminUser])
def appointment_auto_complete(request):
    try:
        providers = Provider.objects.all()
        for provider in providers:
            with schema_context(provider.schema_name):

                now = datetime_helper.now()
                appointments_to_update = Appointment.objects.filter((
                        Q(status=AppointmentStatusChoices.ACTIVE.value) &
                        Q(start_date__lt=now.date()) |
                        (Q(start_date=now.date()) & Q(end_time__lte=now.time()))))
                if appointments_to_update.exists():
                    # Update appointment and related booking status in bulk
                    appointments_to_update.update(status=AppointmentStatusChoices.COMPLETE.value)
                    bookings_to_update = appointments_to_update.filter(booking__isnull=False).values_list('booking__id',
                                                                                                          flat=True)
                    bookings_to_update.update(status=BookingStatusChoices.COMPLETE.value)

                bookings = Booking.objects.filter((
                        Q(status=BookingStatusChoices.ACTIVE.value) &
                        Q(start_date__lt=now.date()) |
                        (Q(start_date=now.date()) & Q(end_time__lte=now.time()))))
                if bookings:
                    bookings.update(status=BookingStatusChoices.COMPLETE.value)
        return Response({'msg': 'Status Updated'}, status=200)
    except Exception as e:
        return Response(f'Failed in appointment_auto_complete because of {str(e)}', status=500)
